Micron Document
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| SparkN0de-git | SparkN0de |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


Commit 4c8d7d11a677426c2496c51e5b03849c5b42c65f


Parents : 461d0fc
Author : Ivan <ivan@quad4.io>
Signature : Invalid signer <e46112d44649266d71fe2193e00a4710>, author is <ivan@quad4.io>
Date : 2026-07-08T16:39:14-05:00

feat(Bot Management): extend bot termination handling on Windows with process termination improvements and add checks for interpreter executable paths in Landlock sandbox. Update self-check to manage logs and storage directories effectively.

Changes
Diff

diff --git a/meshchatx/meshchat.py b/meshchatx/meshchat.py
index 3a8c98b5..62827953 100644
--- a/meshchatx/meshchat.py
+++ b/meshchatx/meshchat.py
@@ -825,7 +825,7 @@ class ReticulumMeshChat:
if not handler.stop_bot(bot_id):
return False, "stop_bot returned False"
- stop_deadline = time.monotonic() + 5.0
+ stop_deadline = time.monotonic() + 8.0
while time.monotonic() < stop_deadline and BotHandler._is_pid_alive(pid):
time.sleep(0.1)
if BotHandler._is_pid_alive(pid):

diff --git a/meshchatx/src/backend/bot_handler.py b/meshchatx/src/backend/bot_handler.py
index 29cf07ad..ff1b6519 100644
--- a/meshchatx/src/backend/bot_handler.py
+++ b/meshchatx/src/backend/bot_handler.py
@@ -393,19 +393,26 @@ class BotHandler:
return False
pid = entry.get("pid")
+ tracked = self.running_bots.get(bot_id) or {}
+ proc = tracked.get("proc")
if pid:
try:
if sys.platform.startswith("win"):
- # Use absolute path if possible to avoid S607
- taskkill = shutil.which("taskkill") or "taskkill"
- # Process may already have exited; suppress "not found" noise.
- subprocess.run(
- [taskkill, "/PID", str(pid), "/T", "/F"],
- check=False,
- timeout=5,
- stdout=subprocess.DEVNULL,
- stderr=subprocess.DEVNULL,
- )
+ if proc is not None:
+ with contextlib.suppress(Exception):
+ proc.terminate()
+ with contextlib.suppress(Exception):
+ proc.wait(timeout=2)
+ if self._is_pid_alive(pid):
+ taskkill = shutil.which("taskkill") or "taskkill"
+ # Process may already have exited; suppress "not found" noise.
+ subprocess.run(
+ [taskkill, "/PID", str(pid), "/T", "/F"],
+ check=False,
+ timeout=5,
+ stdout=subprocess.DEVNULL,
+ stderr=subprocess.DEVNULL,
+ )
else:
try:
os.killpg(pid, 15)
@@ -599,6 +606,8 @@ class BotHandler:
def _is_pid_alive(pid):
if not pid:
return False
+ if sys.platform.startswith("win"):
+ return BotHandler._is_pid_alive_windows(pid)
try:
os.kill(pid, 0)
except OSError:
@@ -618,6 +627,26 @@ class BotHandler:
return False
return True
+ @staticmethod
+ def _is_pid_alive_windows(pid):
+ """Return True only while the process is still running (not exited)."""
+ import ctypes
+ from ctypes import wintypes
+
+ kernel32 = ctypes.windll.kernel32
+ PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
+ STILL_ACTIVE = 259
+ handle = kernel32.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, False, int(pid))
+ if not handle:
+ return False
+ try:
+ code = wintypes.DWORD()
+ if not kernel32.GetExitCodeProcess(handle, ctypes.byref(code)):
+ return False
+ return int(code.value) == STILL_ACTIVE
+ finally:
+ kernel32.CloseHandle(handle)
+
def _reap_process(self, bot_id, pid):
tracked = self.running_bots.get(bot_id) or {}
proc = tracked.get("proc")
@@ -625,7 +654,7 @@ class BotHandler:
with contextlib.suppress(Exception):
proc.poll()
with contextlib.suppress(Exception):
- proc.wait(timeout=2)
+ proc.wait(timeout=5)
return
if not pid:
return

diff --git a/meshchatx/src/backend/landlock_sandbox.py b/meshchatx/src/backend/landlock_sandbox.py
index 18910702..29c5c4c4 100644
--- a/meshchatx/src/backend/landlock_sandbox.py
+++ b/meshchatx/src/backend/landlock_sandbox.py
@@ -236,6 +236,18 @@ def _collect_read_roots() -> list[str]:
existing = _existing_dir(user_site)
if existing:
roots.add(existing)
+ # Allow execve of the running interpreter (uv-managed CPython lives outside
+ # /usr; bots / self-check / rnsh re-spawn sys.executable under Landlock).
+ for candidate in (sys.executable, os.path.realpath(sys.executable)):
+ if not candidate:
+ continue
+ exe_dir = _existing_dir(os.path.dirname(candidate))
+ if exe_dir:
+ roots.add(exe_dir)
+ # Prefer the install prefix (…/cpython-…/) so bin + lib are covered.
+ prefix = _existing_dir(getattr(sys, "base_prefix", None) or sys.prefix)
+ if prefix:
+ roots.add(prefix)
return sorted(roots)

diff --git a/meshchatx/src/backend/self_check.py b/meshchatx/src/backend/self_check.py
index 4c980219..11ada3ca 100644
--- a/meshchatx/src/backend/self_check.py
+++ b/meshchatx/src/backend/self_check.py
@@ -231,6 +231,11 @@ def check_meshchatx_run_module() -> dict[str, str]:
env["MESHCHATX_SELF_CHECK_PROBE_PATH"] = marker
env["PYTHONUNBUFFERED"] = "1"
env["MESHCHAT_SKIP_STORAGE_LOCK"] = "1"
+ # Keep child logs/storage under the temp dir (Landlock RW + no user home writes).
+ env["MESHCHAT_LOG_DIR"] = os.path.join(marker_dir, "logs")
+ env["MESHCHAT_STORAGE_DIR"] = os.path.join(marker_dir, "storage")
+ os.makedirs(env["MESHCHAT_LOG_DIR"], exist_ok=True)
+ os.makedirs(env["MESHCHAT_STORAGE_DIR"], exist_ok=True)
if _is_frozen_executable():
cmd = [
@@ -294,7 +299,7 @@ def check_subprocess_spawn() -> dict[str, str]:
if result.returncode != 0:
return _status(
False,
- f"spawn exited {result.returncode}: {(result.stderr or '')[-300:]}",
+ f"spawn exited {result.returncode}: {(result.stderr or result.stdout or '')[-300:]}",
)
if "meshchatx-spawn-ok" not in (result.stdout or ""):
return _status(False, f"Unexpected spawn output: {result.stdout!r}")

diff --git a/scripts/e2e/start-e2e-stack.sh b/scripts/e2e/start-e2e-stack.sh
index 65762730..2301fad3 100755
--- a/scripts/e2e/start-e2e-stack.sh
+++ b/scripts/e2e/start-e2e-stack.sh
@@ -6,6 +6,9 @@ cd "$ROOT"
export E2E_BACKEND_PORT="${E2E_BACKEND_PORT:-18079}"
export MESHCHAT_NO_HTTPS=1
+# E2E exercises /api/v1/self-test (subprocess + bots). Keep Landlock off so
+# uv-managed interpreters and temp paths are not a sandbox variable.
+export MESHCHAT_LANDLOCK=0
BACKEND_PORT="$E2E_BACKEND_PORT"
VITE_HOST="${E2E_VITE_HOST:-127.0.0.1}"
VITE_PORT="${E2E_VITE_PORT:-5173}"

diff --git a/tests/backend/test_landlock_sandbox.py b/tests/backend/test_landlock_sandbox.py
index 868b1d94..e65c786f 100644
--- a/tests/backend/test_landlock_sandbox.py
+++ b/tests/backend/test_landlock_sandbox.py
@@ -1,5 +1,6 @@
# SPDX-License-Identifier: 0BSD
+import os
import sys
from unittest.mock import patch
@@ -80,3 +81,15 @@ def test_landlock_kernel_supported_on_linux():
def test_collect_read_roots_includes_proc_for_psutil():
roots = ll._collect_read_roots()
assert "/proc" in roots
+
+
+def test_collect_read_roots_includes_interpreter_prefix():
+ roots = ll._collect_read_roots()
+ exe = os.path.realpath(sys.executable)
+ prefix = os.path.realpath(getattr(sys, "base_prefix", None) or sys.prefix)
+ assert any(
+ exe == root or exe.startswith(root.rstrip("/") + "/") for root in roots
+ ), f"executable {exe!r} not covered by {roots!r}"
+ assert any(
+ prefix == root or prefix.startswith(root.rstrip("/") + "/") for root in roots
+ ), f"prefix {prefix!r} not covered by {roots!r}"

diff --git a/tests/e2e/smoke.spec.js b/tests/e2e/smoke.spec.js
index 9aee5dd4..7b790f30 100644
--- a/tests/e2e/smoke.spec.js
+++ b/tests/e2e/smoke.spec.js
@@ -32,7 +32,10 @@ test.describe("MeshChatX E2E (Vite + Python backend)", () => {
];
for (const key of keys) {
expect(body[key], key).toBeDefined();
- expect(body[key].status, key).toBe("ok");
+ expect(
+ body[key].status,
+ `${key}: ${body[key].reason || "(no reason)"}`,
+ ).toBe("ok");
}
});


──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────